<!DOCTYPE html>
<html lang="en">
	<head>
		<title>three.js webgl - sprites nodes</title>
		<meta charset="utf-8">
		<meta name="viewport" content="width=device-width, user-scalable=no, minimum-scale=1.0, maximum-scale=1.0">
		<link type="text/css" rel="stylesheet" href="main.css">
	</head>
	<body>

		<div id="container"></div>
		<div id="info">
			<a href="https://threejs.org" target="_blank" rel="noopener">three.js</a> - Node-Based Sprites
		</div>

		<script type="module">

			import * as THREE from '../build/three.module.js';

			import { OrbitControls } from './jsm/controls/OrbitControls.js';
			import { NodeMaterialLoader, NodeMaterialLoaderUtils } from './jsm/loaders/NodeMaterialLoader.js';

			import {
				NodeFrame,
				SpriteNodeMaterial,
				MathNode,
				OperatorNode,
				TextureNode,
				Vector2Node,
				TimerNode,
				FunctionNode,
				FunctionCallNode,
				PositionNode,
				UVNode
			} from './jsm/nodes/Nodes.js';

			const container = document.getElementById( 'container' );

			let renderer, scene, camera;
			const clock = new THREE.Clock(), fov = 50;
			const frame = new NodeFrame();
			let sprite1, sprite2, sprite3;
			let walkingManTexture, walkingManTextureURL;
			const library = {};
			let controls;

			window.addEventListener( 'load', init );

			function init() {

				//
				// Renderer / Controls
				//

				renderer = new THREE.WebGLRenderer( { antialias: true } );
				renderer.setPixelRatio( window.devicePixelRatio );
				renderer.setSize( window.innerWidth, window.innerHeight );
				container.appendChild( renderer.domElement );

				scene = new THREE.Scene();
				scene.fog = new THREE.Fog( 0x0000FF, 70, 150 );

				camera = new THREE.PerspectiveCamera( fov, window.innerWidth / window.innerHeight, 1, 1000 );
				camera.position.z = 100;

				controls = new OrbitControls( camera, renderer.domElement );
				controls.minDistance = 50;
				controls.maxDistance = 200;

				//
				// SpriteNode
				//

				// https://openclipart.org/detail/239883/walking-man-sprite-sheet
				walkingManTextureURL = "textures/WalkingManSpriteSheet.png";

				walkingManTexture = new THREE.TextureLoader().load( walkingManTextureURL );
				walkingManTexture.wrapS = walkingManTexture.wrapT = THREE.RepeatWrapping;

				library[ walkingManTextureURL ] = walkingManTexture;

				// horizontal sprite-sheet animator

				function createHorizontalSpriteSheetNode( hCount, speed ) {

					const speedNode = new Vector2Node( speed, 0 ); // frame per second
					const scale = new Vector2Node( 1 / hCount, 1 ); // 8 horizontal images in sprite-sheet

					const uvTimer = new OperatorNode(
						new TimerNode(),
						speedNode,
						OperatorNode.MUL
					);

					const uvIntegerTimer = new MathNode(
						uvTimer,
						MathNode.FLOOR
					);

					const uvFrameOffset = new OperatorNode(
						uvIntegerTimer,
						scale,
						OperatorNode.MUL
					);

					const uvScale = new OperatorNode(
						new UVNode(),
						scale,
						OperatorNode.MUL
					);

					const uvFrame = new OperatorNode(
						uvScale,
						uvFrameOffset,
						OperatorNode.ADD
					);

					return uvFrame;

				}

				// sprites

				const spriteWidth = 20, spriteHeight = 20;

				scene.add( sprite1 = new THREE.Sprite( new SpriteNodeMaterial() ) );
				sprite1.scale.x = spriteWidth;
				sprite1.scale.y = spriteHeight;
				sprite1.material.color = new TextureNode( walkingManTexture );
				sprite1.material.color.uv = createHorizontalSpriteSheetNode( 8, 10 );

				scene.add( sprite2 = new THREE.Sprite( new SpriteNodeMaterial() ) );
				sprite2.position.x = 30;
				sprite2.scale.x = spriteWidth;
				sprite2.scale.y = spriteHeight;
				sprite2.material.color = new TextureNode( walkingManTexture );
				sprite2.material.color.uv = createHorizontalSpriteSheetNode( 8, 30 );
				sprite2.material.color = new MathNode( sprite2.material.color, MathNode.INVERT );
				sprite2.material.spherical = false; // look at camera horizontally only, very used in vegetation
				// horizontal zigzag sprite
				sprite2.material.position = new OperatorNode(
					new OperatorNode(
						new MathNode( new TimerNode( 3 ), MathNode.SIN ), // 3 is speed (time scale)
						new Vector2Node( .3, 0 ), // horizontal scale (position)
						OperatorNode.MUL
					),
					new PositionNode(),
					OperatorNode.ADD
				);

				const sineWaveFunction = new FunctionNode( [
					// https://stackoverflow.com/questions/36174431/how-to-make-a-wave-warp-effect-in-shader
					"vec2 sineWave(vec2 uv, vec2 phase) {",
					// wave distortion
					"	float x = sin( 25.0*uv.y + 30.0*uv.x + 6.28*phase.x) * 0.01;",
					"	float y = sin( 25.0*uv.y + 30.0*uv.x + 6.28*phase.y) * 0.03;",
					"	return vec2(uv.x+x, uv.y+y);",
					"}"
				].join( "\n" ) );

				scene.add( sprite3 = new THREE.Sprite( new SpriteNodeMaterial() ) );
				sprite3.position.x = - 30;
				sprite3.scale.x = spriteWidth;
				sprite3.scale.y = spriteHeight;
				sprite3.material.color = new TextureNode( walkingManTexture );
				sprite3.material.color.uv = new FunctionCallNode( sineWaveFunction, {
					"uv": createHorizontalSpriteSheetNode( 8, 10 ),
					"phase": new TimerNode()
				} );
				sprite3.material.fog = true;

				//
				//	Test serialization
				//

				spriteToJSON( sprite1 );
				spriteToJSON( sprite2 );
				spriteToJSON( sprite3 );

				//
				// Events
				//

				window.addEventListener( 'resize', onWindowResize );

				onWindowResize();
				animate();

			}

			function spriteToJSON( sprite ) {

				// serialize

				const json = sprite.material.toJSON();

				// replace uuid to url (facilitates the load of textures using url otherside uuid)

				NodeMaterialLoaderUtils.replaceUUID( json, walkingManTexture, walkingManTextureURL );

				// deserialize

				const material = new NodeMaterialLoader( null, library ).parse( json );

				// replace material

				sprite.material.dispose();

				sprite.material = material;

			}

			function onWindowResize() {

				const width = window.innerWidth, height = window.innerHeight;

				camera.aspect = width / height;
				camera.updateProjectionMatrix();

				renderer.setSize( width, height );

			}

			function animate() {

				const delta = clock.getDelta();

				// update material animation and/or gpu calcs (pre-renderer)
				frame.update( delta )
					.updateNode( sprite1.material )
					.updateNode( sprite2.material )
					.updateNode( sprite3.material );

				// rotate sprite
				sprite3.rotation.z -= Math.PI * .005;

				renderer.render( scene, camera );

				requestAnimationFrame( animate );

			}

		</script>

	</body>
</html>
